import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

/**
 * @author 516750653
 */
public class NetWork {


    private static final byte ONLINE = 1;
    private static final byte OFFLINE = 2;
    private static final byte BLOCK = 3;
    private static final byte TRANSACTION = 4;
    private static final byte GET_BLOCK = 5 ;
    private static final byte GET_TRANSACTION = 6 ;

    private static final int VERSION = 1;

    private static final int PORT = 8555;

    private static final int SERVER = 8666;

    private List<Node> nodeList;

    private static Thread thread;

    private static Thread serverThread;

    public static final BigInteger FIRST = new BigInteger("6848680860684195727532225322922804038594051413509280363258632032461236");

    private static final long BLOCK_MAX_SIZE = 10 * 1024L * 1024L;

    public synchronized void broadCastString(String msg) {
        try {
            broadCast(msg.getBytes());
        } catch (Exception ignored) {

        }
    }


    public synchronized void broadCastBlock(Block block) {

        try {
            ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
            DataOutputStream outputStream = new DataOutputStream(byteOutputStream);

            outputStream.writeInt(VERSION);
            outputStream.writeByte(BLOCK);

            Header header = block.getHeader();
            byte[] ser = header.serialize();
            outputStream.writeInt(header.getHeight());
            outputStream.write(ser);

            List<TransactionId> itemArray = block.getTransactionIdList();
            for (TransactionId transactionId : itemArray) {
                outputStream.write(transactionId.getHash());
            }

            byte[] bytes = byteOutputStream.toByteArray();
            broadCast(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void broadCastTransaction(byte[] transaction) {

        try {
            ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
            DataOutputStream outputStream = new DataOutputStream(byteOutputStream);

            outputStream.writeInt(VERSION);
            outputStream.writeByte(TRANSACTION);
            outputStream.write(transaction);

            byte[] bytes = byteOutputStream.toByteArray();
            broadCast(bytes);
        } catch (Exception ignored) {

        }
    }


    private synchronized void broadCast(byte[] msg) throws Exception {

        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        DataOutputStream outputStream = new DataOutputStream(byteOutputStream);
        outputStream.writeInt(msg.length);
        outputStream.write(msg);
        outputStream.flush();

        byte[] bytes = byteOutputStream.toByteArray();
        DatagramSocket ds = new DatagramSocket();
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("255.255.255.255"), PORT);
        ds.send(dp);
        ds.close();
    }


    public synchronized void start() {

        thread = new Thread(new ReceiveBroadCast());
        thread.start();

        serverThread = new Thread(NetWork::serverSocket);
        serverThread.start();

        broadCastString("online,my server port is " + SERVER);
        // 广播上线消息
    }

    public synchronized void stop() {
        thread.interrupt();
        thread.stop();

        serverThread.interrupt();
        serverThread.stop();

        broadCastString("offline");
        // 广播下线消息
    }


    private class ReceiveBroadCast implements Runnable {
        @Override
        public void run() {

            try {
                byte[] buffer = new byte[4096];
                DatagramSocket ds = new DatagramSocket(PORT);
                System.out.println("监听广播消息...");
                while (true) {

                    try {
                        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                        ds.receive(packet);

                        System.out.println(packet.getAddress() + ":" + packet.getPort());

                        byte[] data = packet.getData();

                        ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
                        DataInputStream dataInputStream = new DataInputStream(inputStream);

                        int packageLen = dataInputStream.readInt();
                        int netVersin = dataInputStream.readInt();
                        int mesType = dataInputStream.readByte();

                        byte[] dataNew = new byte[packageLen];
                        System.arraycopy(data, 4, dataNew, 0, packageLen);

                        if (mesType == TRANSACTION) {
                            System.out.println("广播消息::TRANSACTION");
                            receiveTransaction(dataNew);
                        } else if (mesType == BLOCK) {
                            System.out.println("广播消息::BLOCK");
                            receiveBlock(dataNew);
                        } else {
                            System.out.println(new String(dataNew));
                        }
                    } catch (Exception ignored) {
                        ignored.printStackTrace();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    private static void serverSocket() {

        try {

            ServerSocket ss = new ServerSocket(SERVER);
            System.out.println("启动服务器....");

            while (true) {
                Socket s = ss.accept();
                System.out.println("客户端:" + s.getInetAddress() + "已连接到服务器");
                BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
                //读取客户端发送来的消息
                String mess = br.readLine();
                System.out.println("客户端：" + mess);
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
                bw.write(mess + "\n");
                bw.flush();
            }
        } catch (Exception e) {

        }

    }


    private static boolean check(String data) {
        return true;
    }


    private void receiveTransaction(byte[] data) {

        int length = data.length - 5;
        byte[] content = new byte[length];

        System.arraycopy(data, 5, content, 0, length);
        Transaction transaction = new Transaction();
        try {

            boolean check = Transaction.check(content, transaction);
            if (check) {
                Pool.getInstance().add(transaction);
            }

            System.out.println(transaction);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void receiveBlock(byte[] data) throws Exception {

        int length = data.length - 5;
        byte[] content = new byte[length];
        System.arraycopy(data, 5, content, 0, length);

        ByteArrayInputStream byteInputStream = new ByteArrayInputStream(content);
        DataInputStream dataInputStream = new DataInputStream(byteInputStream);

        Header header = Header.readHeader(dataInputStream);
        List<TransactionId> transactionIdList = new ArrayList<>();

        int tc = (data.length - 80) / 32;

        for (int i = 0; i < tc; i++) {
            byte[] hash = new byte[32];
            dataInputStream.read(hash);
            transactionIdList.add(new TransactionId(hash));
        }


        byte[] ser = header.serialize();
        byte[] hash = Hash256.hash(Hash256.hash(ser));

        BigInteger check = new BigInteger(hash);

        if (check.equals(FIRST)) {
            System.out.println("this is the first block");
        } else {

        }


        byte[] merkleRoot = Miner.buildMerkleRoot(transactionIdList);
        if (Arrays.equals(merkleRoot, header.getMerkleRoot())) {
            System.out.println("hashKeyList is right, size==" + transactionIdList.size());
        } else {
            System.out.println("hashKeyList is error");
        }

        Block block = new Block(header, transactionIdList);

        // 检查header头和block是否满足要求
        boolean acceptHeader = HeaderList.getInstance().accept(header);
        boolean acceptBlock = check(block);

        if (acceptHeader && acceptBlock) {

            System.out.println("accept new block");

            //  添加HeaderList
            HeaderList.getInstance().addHeader(header);

            // 添加区块
            BlockChain.getInstance().addBlock(block);

            //  存储新区快的交易
            MemoryDb.getInstance().accept(block);

            // 客户端查询是否有属于自己的收款信息
            Client.getInstance().extractFromBlock(block);

            // 更新交易池中的交易
            Pool.getInstance().tryCleanCache(block);

        } else {
            System.out.println("error: not accept");
        }

    }


    private boolean check(Block block) {

        //  第一笔交易是CoinBase交易
        //  其他交易不是CoinBase交易
        //  检测交易的preTransactions的Transactions的cost是否已经被花费
        //  这里需要检测两个地方
        //  1:是本区块内是否存在双花   (input[i][transactionId],pubKeyToAddress) 是唯一的。
        //  2:和历史的对比是否存在双花

        //  大小是否合法
        int blockSize = 0;

        //  检测block是否合法
        List<TransactionId> transactionIdList = block.getTransactionIdList();
        if (transactionIdList == null || transactionIdList.size() == 0) {
            return false;
        }

        Transaction first = Pool.getInstance().getTransaction(transactionIdList.get(0));
        if (first == null) {
            throw new Error("first不应该为空");
        }

        if (first.getInputs() != null) {
            return false;
        }

        blockSize += first.getSize();

        int size = transactionIdList.size();
        HashSet<TransactionId> set = new HashSet(transactionIdList);

        // 存在重复的交易
        if (set.size() < transactionIdList.size()) {
            return false;
        }


        HashSet<DoubleCostCheck.Item> costItems = new HashSet<>();

        // 检测本区块内部是否存在双花 以及区块大小是否满足要求
        for (int i = 1; i < size; i++) {

            TransactionId cur = transactionIdList.get(i);
            Transaction item = Pool.getInstance().getTransaction(cur);
            if (item == null) {
                throw new Error("item不应该为空");
            }

            for (Input input : item.getInputs()) {

                DoubleCostCheck.Item costItem = new DoubleCostCheck.Item(input.getTransactionId(), Address.convertPubKeyToAddress(item.getPubKey()));

                if (costItems.contains(costItem)) {
                    return false;
                }
                costItems.add(costItem);
            }

            blockSize += item.getSize();
        }

        System.out.println("区块大小为blockSize===" + blockSize);
        if (blockSize > BLOCK_MAX_SIZE) {
            return false;
        }

        for (int i = 1; i < size; i++) {

            TransactionId cur = transactionIdList.get(i);
            Transaction item = Pool.getInstance().getTransaction(cur);
            if (item == null) {
                throw new Error("item不应该为空");
            }

            List<Input> preTransactions = item.getInputs();
            if (preTransactions.size() == 0) {
                return false;
            }

            String address = Address.convertPubKeyToAddress(item.getPubKey());

            for (Input input : preTransactions) {

                // 根据hashId去DB里面取已确定的交易
                TransactionId preTransactionId = input.getTransactionId();
                Transaction check = MemoryDb.getInstance().getTransaction(preTransactionId);
                if (check == null) {
                    return false;
                }

                //  应该有一笔cost等于input的值
                List<Cost> costList = check.getCosts();
                boolean flag = false;

                for (Cost cost : costList) {
                    if (cost.getAddress().equals(address) && cost.getAmount() == input.getAmount()) {
                        flag = true;
                        break;
                    }
                }

                if (!flag) {
                    return false;
                }

                //  如果已被使用则返回false
                if (DoubleCostCheck.getInstance().checkUsed(new TransactionId(check.getHash()), address)) {
                    return false;
                }
            }
        }

        block.setBlockSize(blockSize);
        return true;
    }

}
